Skip to content

fix(rows): return 404 when a row is not reachable through the view or table - #3178

Open
rubenvdlinde wants to merge 1 commit into
mainfrom
fix/noid/view-row-404
Open

rubenvdlinde wants to merge 1 commit into
mainfrom
fix/noid/view-row-404

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Oct 30, 2026 •

Copy link
Copy Markdown

Updating or deleting a row through a view answered with a 500 when the row was not in that view. The same happened when you deleted through a view that does not exist, and when you updated or deleted a row through a table it does not belong to. In all these cases the row simply isn't there for you, so the answer should be a 404.

@remko48 pointed this out on #3075: it "was already a 500 for normal views too". The 404 guard that came out of that only covers views without a table. This pull request does the same for normal views and tables.

What changes

In RowService::updateSet() and RowService::delete():

  • A row that is not in the view throws NotFoundError instead of InternalError.
  • In delete(), a view that does not exist throws NotFoundError. updateSet() already did this.
  • A row that does not belong to the given table throws NotFoundError.

Permission failures still throw PermissionError and stay 403. The controllers did not need changes: api/1, api/2 and the internal row routes already map NotFoundError to 404. Api1Controller::updateRow() does not catch it itself, but PermissionMiddleware::afterException() turns it into a 404.

How it was tested

New unit test tests/unit/Service/RowServiceRowNotFoundTest.php, one test per case plus one that checks a missing delete permission is still a PermissionError. Without the fix, the five not-found tests fail with InternalError:

Without the fix: Tests: 6, Assertions: 7, Failures: 5.
With the fix:    OK (6 tests, 12 assertions)
Full unit suite, PostgreSQL and SQLite: OK (428 tests, 846 assertions)
composer cs:check: no files to fix
psalm: No errors found!

On a local instance, with row 12 in table 3 but outside the filter of view 1, and table 4 that does not own row 12:

Request Before After
api/1 PUT /rows/12?viewId=1 500 404
api/1 DELETE /views/1/rows/12 500 404
api/2 PUT views/1/rows/12 500 404
api/2 DELETE views/1/rows/12 500 404
internal PUT /row/12?viewId=1 500 404
internal DELETE /view/1/row/12 500 404
api/2 PUT tables/4/rows/12 500 404
api/2 DELETE tables/4/rows/12 500 404

🖼️ Screenshots

Backend only, no visual change.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔙 Backport requests are created or not needed: /backport to stableX.X
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

… table

Updating or deleting a row through a view returned an internal error,
and so a 500, when the row was not in that view. Deleting through a view
that does not exist, and updating or deleting a row through a table it
does not belong to, did the same. These now throw NotFoundError, which
every API layer already maps to 404. Permission failures stay 403.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Ruben van der Linde <ruben@conduction.nl>
@rubenvdlinde
rubenvdlinde force-pushed the fix/noid/view-row-404 branch from 9fe0711 to d5805fd Compare November 3, 2026 14:51
@rubenvdlinde
rubenvdlinde marked this pull request as ready for review November 6, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants